{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Workforce Scheduling Problem\n",
    "\n",
    "## Objective and Prerequisites\n",
    "\n",
    "In this example, you’ll learn how to solve a critical, central problem in the services industry: workforce scheduling. We’ll demonstrate how you can use mathematical optimization to generate an optimal workforce schedule that meets your business requirements, maximizes employee fairness and satisfaction, and minimizes the number of temporary workers your company needs to hire.\n",
    "\n",
    "This modeling example is at the advanced level, where we assume that you know Python and the Gurobi Python API and that you have advanced knowledge of building mathematical optimization models. Typically, the objective function and/or constraints of these examples are complex or require advanced features of the Gurobi Python API.\n",
    "\n",
    "**Download the Repository** <br />\n",
    "You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip). \n",
    "\n",
    "## Motivation\n",
    "People are the most important asset for companies in the services industry as well as their largest source of costs.\n",
    "Workforce allocation and personnel scheduling deal with the arrangement of work schedules  and the assignment of personnel shifts in order to cover the demand for resources that vary over time.\n",
    "\n",
    "These problems are very important for companies in the services industries who rely on workforce resources such as:\n",
    "- Telephone operators\n",
    "- Hospital nurses\n",
    "- Policemen\n",
    "- Transportation personnel (plane crews, bus drivers, etc.)\n",
    "- Hospitality personnel\n",
    "- Restaurant personnel\n",
    "\n",
    "## Problem Description\n",
    "\n",
    "Consider a service business, like a restaurant, that develops its workforce plans for the next two weeks (considering a 7-day week). The service requires only one set of skills. There are a number of employed workers with the same set of skills and with identical productivity that are available to work on some of the days during the two-week planning horizon. There is only one shift per workday. Each shift may have different resource (worker) requirements on each workday. The service business may hire extra (temp) workers from an agency to satisfy shift requirements. The service business wants to minimize the number of extra workers that it needs to hire, and as a secondary objective, it wants to balance the workload of employed workers to ensure fairness."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model Formulation\n",
    "Mathematical programming is a declarative approach where the modeler formulates a mathematical optimization problem that captures the key features of a complex decision problem. The Gurobi Optimizer solves the mathematical optimization problem using state-of-the-art mathematics and computer science.\n",
    "\n",
    "A mathematical optimization model has five components:\n",
    "- Sets.\n",
    "- Parameters.\n",
    "- Decision variables.\n",
    "- Constraints.\n",
    "- Objective function(s).\n",
    "\n",
    "### Sets and Indices\n",
    "\n",
    "$s \\in \\text{shifts}$: Index and set of shifts.\n",
    "\n",
    "$w \\in workers$: Index and set of employed workers.\n",
    "\n",
    "$availability= \\{(w,s): w \\in workers \\; \\text{is available for} \\; s \\in \\text{shifts} \\}$: This set determines that the employed worker  $w$ is available to work on shift $s$.\n",
    "\n",
    "### Parameters\n",
    "\n",
    "$\\text{shiftRequirements}(s) \\in \\mathbb{N}$: This parameter represents the number of workers required at each shift \n",
    "$s \\in \\text{shifts}$.\n",
    "\n",
    "$pay(w) \\in \\mathbb{R^{+}}$: This parameter is the salary per day of each worker $w \\in workers$. \n",
    "\n",
    "$relTol$: This relative tolerance parameter  makes it possible to deviate from the optimal objective function value of the primary objective, when solving the model for the secondary objective. (See explanation below of the multi-objective optimization process).\n",
    "\n",
    "### Computed Parameters\n",
    "These parameters are derived from the input parameters and/or from the optimization process.\n",
    "\n",
    "$Opt(totSlack)$: This parameter is the minimum value of extra workers required to fill demand. \n",
    "\n",
    "### Decision Variables\n",
    "\n",
    "$x(w,s) \\in \\{0,1\\}$: This variable equals 1 if worker $w \\in workers$ is assigned to shift $s \\in \\text{shifts}$, \n",
    "and 0 otherwise.\n",
    "This decision variable is defined over the $availability$ set.\n",
    "\n",
    "$Slack(s) \\geq 0$: This variable is the number of extra workers required to satisfy the requirements of shift $s$.\n",
    "\n",
    "### Auxiliary Variables\n",
    "Auxiliary variables are derived from the values of the decision variables.\n",
    "\n",
    "$totSlack$: This variable counts the total number of extra workers required to satisfy the demand for shift workers.\n",
    "\n",
    "$\\text{totShifts}(w)$: This variable counts the total number of shifts worked by each worker $w \\in workers$.\n",
    "\n",
    "$\\text{minShift}$: This variable determines the minimum number of shifts allocated to workers. \n",
    "\n",
    "$\\text{maxShift}$: This variable  determines the maximum number of shifts allocated to workers.\n",
    "\n",
    "## Optimization Process\n",
    "\n",
    "In this modeling example, we tackle a multi-objective model where the primary objective is to minimize the number of extra workers required to satisfy shift requirements, and the secondary objective is to minimize the maximum difference in the number of shifts worked between any pair of workers. This secondary objective balances the workload allocated to workers.\n",
    " \n",
    "The hierarchical multi-objective optimization approach will first solve the following model with the primary objective.\n",
    "\n",
    "### Objective Function\n",
    "\n",
    "- **Primary objective:** The objective function is to minimize the number of extra workers allocated. \n",
    "\n",
    "\\begin{equation}\n",
    "\\text{Min} \\quad totSlack\n",
    "\\tag{0}\n",
    "\\end{equation}\n",
    "\n",
    "### Constraints\n",
    "\n",
    "- **Shift requirements:**  All shifts requirements most be satisfied.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{w \\: \\in \\: workers} x(w,s)  +  Slack(s) = \\text{shiftRequirements}(s) \\quad \\forall \\; s \\in \\text{shifts}\n",
    "\\tag{1}\n",
    "\\end{equation}\n",
    "\n",
    "- **Temps:** Compute the total number of extra workers required.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{s \\: \\in \\: \\text{shifts}} Slack(s) =  totSlack \n",
    "\\tag{2}\n",
    "\\end{equation}\n",
    "\n",
    "- **Shifts:**  Compute the total number of shifts for each worker.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{s \\: \\in \\: \\text{shifts}} x(w,s)  = \\text{totShifts}(w) \\quad \\forall \\; w \\in workers\n",
    "\\tag{3}\n",
    "\\end{equation}\n",
    "\n",
    "- **Binary and non-negativity constraints:**\n",
    "\n",
    "$$\n",
    "x(w,s) \\in \\{0, 1 \\}  \\quad \\forall \\; (w,s) \\in availability\n",
    "$$\n",
    "\n",
    "$$\n",
    "Slack(s) \\geq 0 \\quad  \\forall \\; s \\in \\text{shifts}\n",
    "$$\n",
    "\n",
    "$$\n",
    "\\text{totShifts}(w) \\geq 0 \\quad \\forall \\; w \\in workers\n",
    "$$\n",
    "\n",
    "$$\n",
    "totSlack  \\geq 0\n",
    "$$\n",
    "\n",
    "The parameter $Opt(totSlack)$ is set to be equal to the minimum number of extra workers required to satisfy demand. That is, $Opt(totSlack) = totSlack^{*} $, where $totSlack^{*} $ is the optimal value of the variable $totSlack$.\n",
    "\n",
    "For a given relative tolerance of primary objective $relTol$, we now solve the following model where we minimize the secondary objective.\n",
    "\n",
    "\n",
    "### Objective Function\n",
    "\n",
    "- **Secondary objective:** The objective function is to minimize the difference between the maximum number of shifts allocated to a worker, and the minimum number of shifts allocated to a worker. \n",
    "\n",
    "\\begin{equation}\n",
    "\\text{Min} \\quad ( \\text{maxShift} -  \\text{minShift} )\n",
    "\\tag{4}\n",
    "\\end{equation}\n",
    "\n",
    "### Constraints\n",
    "\n",
    "- **Shift requirements:**  All shifts requirements most be satisfied.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{w \\: \\in \\: workers} x(w,s)  +  Slack(s) = \\text{shiftRequirements}(s) \\quad \\forall \\; s \\in \\text{shifts}\n",
    "\\tag{1}\n",
    "\\end{equation}\n",
    "\n",
    "\n",
    "- **Relative tolerance:**  Ensure that the total number of extra workers (totSlack) minimized by the primary objective is satisfied up to a tolerance percentage.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{s \\: \\in \\: shifts} Slack(s) \\leq (1+relTol)*Opt(totSlack)\n",
    "\\tag{5}\n",
    "\\end{equation}\n",
    "\n",
    "- **Shifts:**  Compute the total number of shifts for each worker.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{s \\: \\in \\: \\text{shifts}} x(w,s)  = \\text{totShifts}(w) \\quad \\forall \\; w \\in workers\n",
    "\\tag{3}\n",
    "\\end{equation}\n",
    "\n",
    "- **Minimum shifts:**  Compute the minimum number of shifts allocated to an employed worker.\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{minShift} = \\text{Min} \\{\\text{totShifts}(w): \\ w \\; in \\; workers \\}  \n",
    "\\tag{6}\n",
    "\\end{equation}\n",
    "\n",
    "\n",
    "- **Maximum shifts:**  Compute the maximum number of shifts allocated to an employed worker.\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{maxShift} = \\text{Max} \\{\\text{totShifts}(w): \\ w \\; in \\; workers \\}\n",
    "\\tag{7}\n",
    "\\end{equation}\n",
    "\n",
    "- **Binary and non-negativity constraints:**\n",
    "\n",
    "$$\n",
    "x(w,s) \\in \\{0, 1 \\}  \\quad \\forall \\; (w,s) \\in availability\n",
    "$$\n",
    "\n",
    "$$\n",
    "Slack(s) \\geq 0 \\quad  \\forall \\; s \\in \\text{shifts}\n",
    "$$\n",
    "\n",
    "$$\n",
    "\\text{totShifts}(w) \\geq 0 \\quad   \\forall \\; w \\in workers\n",
    "$$\n",
    "\n",
    "$$\n",
    "\\text{maxShift},  \\text{minShift}   \\geq 0\n",
    "$$\n",
    "\n",
    "This optimization process is done automatically by the Gurobi Python API when using the following method: \n",
    "\n",
    "\"Model.setObjectiveN()\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Python Implementation\n",
    "\n",
    "We now import the Gurobi Python Module. Then, we initialize the data structures with the given data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install gurobipy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# This command imports the Gurobi functions and classes.\n",
    "\n",
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "\n",
    "import pandas as pd\n",
    "from pylab import *\n",
    "import matplotlib\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Input Data\n",
    "\n",
    "We define all the input data of the model.\n",
    "\n",
    "### Parameter\n",
    "\n",
    "$\\text{shiftRequirements}(s) \\in \\mathbb{N}$: This parameter represents the number of workers required at each shift \n",
    "$s \\in \\text{shifts}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Number of workers required for each shift.\n",
    "shifts, shiftRequirements = gp.multidict({\n",
    "  \"Mon1\":  3,\n",
    "  \"Tue2\":  2,\n",
    "  \"Wed3\":  4,\n",
    "  \"Thu4\":  4,\n",
    "  \"Fri5\":  5,\n",
    "  \"Sat6\":  6,\n",
    "  \"Sun7\":  5,\n",
    "  \"Mon8\":  2,\n",
    "  \"Tue9\":  2,\n",
    "  \"Wed10\": 3,\n",
    "  \"Thu11\": 4,\n",
    "  \"Fri12\": 6,\n",
    "  \"Sat13\": 7,\n",
    "  \"Sun14\": 5 })"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Parameter\n",
    "\n",
    "$pay(w) \\in \\mathbb{R^{+}}$: This parameter is the salary per day of each worker $w \\in workers$. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Amount each worker is paid to work one shift.\n",
    "workers, pay = gp.multidict({\n",
    "  \"Amy\":   10,\n",
    "  \"Bob\":   12,\n",
    "  \"Cathy\": 10,\n",
    "  \"Dan\":   8,\n",
    "  \"Ed\":    8,\n",
    "  \"Fred\":  9,\n",
    "  \"Gu\":    11 })"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Set\n",
    "\n",
    "$availability= \\{(w,s): w \\in workers \\; \\text{is available for} \\; s \\in \\text{shifts} \\}$: This set determines that the employed worker  $w$ is available to work on shift $s$.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Worker availability: defines on which day each employed worker is available.\n",
    "\n",
    "availability = gp.tuplelist([\n",
    "('Amy', 'Tue2'), ('Amy', 'Wed3'), ('Amy', 'Fri5'), ('Amy', 'Sun7'),\n",
    "('Amy', 'Tue9'), ('Amy', 'Wed10'), ('Amy', 'Thu11'), ('Amy', 'Fri12'),\n",
    "('Amy', 'Sat13'), ('Amy', 'Sun14'), ('Bob', 'Mon1'), ('Bob', 'Tue2'),\n",
    "('Bob', 'Fri5'), ('Bob', 'Sat6'), ('Bob', 'Mon8'), ('Bob', 'Thu11'),\n",
    "('Bob', 'Sat13'), ('Cathy', 'Wed3'), ('Cathy', 'Thu4'), ('Cathy', 'Fri5'),\n",
    "('Cathy', 'Sun7'), ('Cathy', 'Mon8'), ('Cathy', 'Tue9'), ('Cathy', 'Wed10'),\n",
    "('Cathy', 'Thu11'), ('Cathy', 'Fri12'), ('Cathy', 'Sat13'),\n",
    "('Cathy', 'Sun14'), ('Dan', 'Tue2'), ('Dan', 'Wed3'), ('Dan', 'Fri5'),\n",
    "('Dan', 'Sat6'), ('Dan', 'Mon8'), ('Dan', 'Tue9'), ('Dan', 'Wed10'),\n",
    "('Dan', 'Thu11'), ('Dan', 'Fri12'), ('Dan', 'Sat13'), ('Dan', 'Sun14'),\n",
    "('Ed', 'Mon1'), ('Ed', 'Tue2'), ('Ed', 'Wed3'), ('Ed', 'Thu4'),\n",
    "('Ed', 'Fri5'), ('Ed', 'Sun7'), ('Ed', 'Mon8'), ('Ed', 'Tue9'),\n",
    "('Ed', 'Thu11'), ('Ed', 'Sat13'), ('Ed', 'Sun14'), ('Fred', 'Mon1'),\n",
    "('Fred', 'Tue2'), ('Fred', 'Wed3'), ('Fred', 'Sat6'), ('Fred', 'Mon8'),\n",
    "('Fred', 'Tue9'), ('Fred', 'Fri12'), ('Fred', 'Sat13'), ('Fred', 'Sun14'),\n",
    "('Gu', 'Mon1'), ('Gu', 'Tue2'), ('Gu', 'Wed3'), ('Gu', 'Fri5'),\n",
    "('Gu', 'Sat6'), ('Gu', 'Sun7'), ('Gu', 'Mon8'), ('Gu', 'Tue9'),\n",
    "('Gu', 'Wed10'), ('Gu', 'Thu11'), ('Gu', 'Fri12'), ('Gu', 'Sat13'),\n",
    "('Gu', 'Sun14')\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model Deployment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Using license file c:\\gurobi\\gurobi.lic\n"
     ]
    }
   ],
   "source": [
    "# Create initial model.\n",
    "m = gp.Model(\"workforce5\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Decision Variables\n",
    "\n",
    "$x(w,s) \\in \\{0,1\\}$: This variable equals 1 if worker $w \\in workers$ is assigned to shift $s \\in \\text{shifts}$, \n",
    "and 0 otherwise.\n",
    "This decision variable is defined over the $availability$ set.\n",
    "\n",
    "$Slack(s) \\geq 0$: This variable is the number of extra workers required to satisfy the requirements of shift $s$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Initialize assignment decision variables.\n",
    "\n",
    "x = m.addVars(availability, vtype=GRB.BINARY, name=\"x\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Slack decision variables determine the number of extra workers required to satisfy the requirements \n",
    "# of each shift\n",
    "slacks = m.addVars(shifts, name=\"Slack\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Auxiliary Variables\n",
    "Auxiliary variables are derived from the values of the decision variables.\n",
    "\n",
    "$totSlack$: This variable counts the total number of extra workers required to satisfy the demand for shift workers.\n",
    "\n",
    "$\\text{totShifts}(w)$: This variable counts the total number of shifts worked by each worker $w \\in workers$.\n",
    "\n",
    "$\\text{minShift}$: This variable determines the minimum number of shifts allocated to workers. \n",
    "\n",
    "$\\text{maxShift}$: This variable  determines the maximum number of shifts allocated to workers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Auxiliary variable totSlack to represent the total number of extra workers required to satisfy the \n",
    "# requirements of all the shifts.\n",
    "totSlack = m.addVar(name='totSlack')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Auxiliary variable totShifts counts the total shifts worked by each employed worker\n",
    "totShifts = m.addVars(workers, name=\"TotShifts\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Constraint\n",
    "\n",
    "$$\\sum_{w \\: \\in \\: workers} x(w,s) \\; + \\; Slack(s) \\; = \\; \\text{shiftRequirements}(s) \\; \\; \\; \\forall \\; s \\in shifts $$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Constraint: All shifts requirements most be satisfied.\n",
    "\n",
    "shift_reqmts = m.addConstrs((x.sum('*',s) + slacks[s] == shiftRequirements[s] for s in shifts), name='shiftRequirement')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Constraint\n",
    "\n",
    "$$ \\sum_{s \\: \\in \\: shifts} Slack(s) \\; = \\; totSlack  $$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Constraint: set the auxiliary variable (totSlack) equal to the total number of extra workers \n",
    "# required to satisfy shift requirements\n",
    " \n",
    "num_temps = m.addConstr(totSlack == slacks.sum(), name='totSlack')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Constraint\n",
    "\n",
    "$$\\sum_{s \\: \\in \\: shifts} x(w,s) \\; = \\; \\text{totShifts}(w) \\; \\; \\forall \\ w \\; in \\; workers$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Constraint: compute the total number of shifts for each worker\n",
    "\n",
    "num_shifts = m.addConstrs((totShifts[w] == x.sum(w,'*') for w in workers), name='totShifts')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The variable $\\text{minShift}$ determines the minimum numbers of shifts allocated to workers. This variable is calculated as follows:\n",
    "$\\text{minShift} = Min \\{\\text{totShifts}(w): \\ w \\; in \\; workers \\} $\n",
    "\n",
    "The variable $\\text{maxShift}$ determines the maximum numbers of shifts allocated to workers. This variable is calculated as follows:\n",
    "$\\text{maxShift} = Max \\{\\text{totShifts}(w): \\ w \\; in \\; workers \\} $"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Auxiliary variables. \n",
    "# minShift is the minimum number of shifts allocated to workers \n",
    "# maxShift is the maximum number of shifts allocated to workers\n",
    "\n",
    "minShift = m.addVar(name='minShift')\n",
    "\n",
    "maxShift = m.addVar(name='maxShift')\n",
    "\n",
    "# Constraint: \n",
    "# The addGenConstrMin() method of the model object m adds a new general constraint that \n",
    "# determines the minimum value among a set of variables. \n",
    "# The first argument is the variable whose value will be equal to the minimum of the other variables, \n",
    "# minShift in this case. \n",
    "# The second argument is the set variables over which the minimum will be taken, (totShifts) in \n",
    "# this case. \n",
    "# Recall that the totShifts variable is defined over the set of worker and determines the number of \n",
    "# shifts that an employed worker will work. The third argument is the name of this constraint.\n",
    "\n",
    "min_constr = m.addGenConstrMin(minShift, totShifts, name='minShift')\n",
    "\n",
    "# Constraint: \n",
    "# Similarly, the addGenConstrMax() method of the model object m adds a new general \n",
    "# constraint that determines the maximum value among a set of variables.\n",
    "\n",
    "max_constr = m.addGenConstrMax(maxShift, totShifts, name='maxShift')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We have a primary and a secondary objective which both aim to minimize."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set global sense for ALL objectives.\n",
    "# This means that all objectives of the model object m are going to be minimized\n",
    "m.ModelSense = GRB.MINIMIZE"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Objective Function\n",
    "\n",
    "The primary objective is to minimize the total number of extra workers required to satisfy shift requirements. \n",
    "$$\n",
    "\\text{Min} \\; totSlack \\; = \\; \\sum_{s \\: \\in \\: shifts} Slack(s) \n",
    "$$\n",
    "\n",
    "For this objective we allow a degradation tolerance of up to certain percentage. If reltol = 0.2, then we allow a degradation\n",
    "tolerance of 20%. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set up primary objective.\n",
    "\n",
    "# The setObjectiveN() method of the model object m allows to define multiple objectives.\n",
    "# The first argument is the linear expression defining the most important objective, called primary \n",
    "# objective, in this case it is the minimization of extra workers required to satisfy shift \n",
    "# requirements. \n",
    "# The second argument is the index of the objective function, we set the index of the primary \n",
    "# objective to be equal to 0. \n",
    "# The third argument is the priority of the objective.\n",
    "# The fourth argument is the relative tolerance to degrade this objective when a lower priority\n",
    "# objective is optimized. The fifth argument is the name of this objective.\n",
    "# A hierarchical or lexicographic approach assigns a priority to each objective, and optimizes \n",
    "# for the objectives in decreasing priority order. \n",
    "# For this problem, we have two objectives, and the primary objective has the highest priority \n",
    "# which is equal to 2. \n",
    "# When the secondary objective is minimized, since the relative tolerance is 0.2, we can only \n",
    "# increase the minimum number of extra workers up to 20%. \n",
    "# For example if the minimum number extra workers is 10, then when optimizing the secondary objective \n",
    "# we can have up to 12 extra workers.\n",
    "\n",
    "m.setObjectiveN(totSlack, index=0, priority=2, reltol=0.2, name='TotalSlack')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The secondary objective is to balance workload among workers. To do so, we minimize the difference between the maximum \n",
    "number of shifts allocated to a worker, and the minimum number of shifts allocated to a worker. That is\n",
    "$$\n",
    "\\text{Min} \\; (\\text{maxShift}  -  \\text{minShift}) \n",
    "$$ "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set up secondary objective.\n",
    "\n",
    "# The secondary objective is called fairness and its goal is to balance the workload assigned \n",
    "# to the employed workers. \n",
    "# To balance the workload assigned to the employed workers, we can minimize the difference \n",
    "# between the maximum number of shifts assigned to an employed worker and the minimum number \n",
    "# of shifts assigned to an employed worker.\n",
    "\n",
    "m.setObjectiveN(maxShift - minShift, index=1, priority=1, name='Fairness')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Save model formulation for inspection\n",
    "\n",
    "m.write('workforce.lp')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (win64)\n",
      "Thread count: 4 physical cores, 8 logical processors, using up to 8 threads\n",
      "Optimize a model with 22 rows, 96 columns and 180 nonzeros\n",
      "Model fingerprint: 0x8f5c4720\n",
      "Model has 2 general constraints\n",
      "Variable types: 24 continuous, 72 integer (72 binary)\n",
      "Coefficient statistics:\n",
      "  Matrix range     [1e+00, 1e+00]\n",
      "  Objective range  [1e+00, 1e+00]\n",
      "  Bounds range     [1e+00, 1e+00]\n",
      "  RHS range        [2e+00, 7e+00]\n",
      "\n",
      "---------------------------------------------------------------------------\n",
      "Multi-objectives: starting optimization with 2 objectives ... \n",
      "---------------------------------------------------------------------------\n",
      "\n",
      "Multi-objectives: applying initial presolve ...\n",
      "---------------------------------------------------------------------------\n",
      "\n",
      "Presolve added 10 rows and 22 columns\n",
      "Presolve time: 0.00s\n",
      "Presolved: 32 rows and 118 columns\n",
      "---------------------------------------------------------------------------\n",
      "\n",
      "Multi-objectives: optimize objective 1 (TotalSlack) ...\n",
      "---------------------------------------------------------------------------\n",
      "\n",
      "Presolve added 7 rows and 0 columns\n",
      "Presolve removed 0 rows and 24 columns\n",
      "Presolve time: 0.00s\n",
      "Presolved: 39 rows, 94 columns, 184 nonzeros\n",
      "Presolved model has 7 SOS constraint(s)\n",
      "Variable types: 16 continuous, 78 integer (64 binary)\n",
      "Found heuristic solution: objective 11.0000000\n",
      "Found heuristic solution: objective 6.0000000\n",
      "\n",
      "Root relaxation: cutoff, 0 iterations, 0.00 seconds\n",
      "\n",
      "Explored 0 nodes (0 simplex iterations) in 0.03 seconds\n",
      "Thread count was 8 (of 8 available processors)\n",
      "\n",
      "Solution count 2: 6 11 \n",
      "\n",
      "Optimal solution found (tolerance 1.00e-04)\n",
      "Best objective 6.000000000000e+00, best bound 6.000000000000e+00, gap 0.0000%\n",
      "---------------------------------------------------------------------------\n",
      "\n",
      "Multi-objectives: optimize objective 2 (Fairness) ...\n",
      "---------------------------------------------------------------------------\n",
      "\n",
      "\n",
      "Loaded user MIP start with objective 10\n",
      "\n",
      "Presolve added 7 rows and 0 columns\n",
      "Presolve removed 0 rows and 15 columns\n",
      "Presolve time: 0.00s\n",
      "Presolved: 40 rows, 103 columns, 241 nonzeros\n",
      "Presolved model has 7 SOS constraint(s)\n",
      "Variable types: 16 continuous, 87 integer (80 binary)\n",
      "\n",
      "Root relaxation: objective 3.333333e-01, 52 iterations, 0.00 seconds\n",
      "\n",
      "    Nodes    |    Current Node    |     Objective Bounds      |     Work\n",
      " Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time\n",
      "\n",
      "     0     0    0.33333    0   16   10.00000    0.33333  96.7%     -    0s\n",
      "H    0     0                       1.0000000    0.33333  66.7%     -    0s\n",
      "     0     0     cutoff    0         1.00000    1.00000  0.00%     -    0s\n",
      "\n",
      "Cutting planes:\n",
      "  Gomory: 8\n",
      "  Implied bound: 1\n",
      "  RLT: 6\n",
      "  Relax-and-lift: 1\n",
      "\n",
      "Explored 1 nodes (79 simplex iterations) in 0.07 seconds\n",
      "Thread count was 8 (of 8 available processors)\n",
      "\n",
      "Solution count 2: 1 10 \n",
      "\n",
      "Optimal solution found (tolerance 1.00e-04)\n",
      "Best objective 1.000000000000e+00, best bound 1.000000000000e+00, gap 0.0000%\n",
      "\n",
      "---------------------------------------------------------------------------\n",
      "Multi-objectives: solved in 0.08 seconds, solution count 3\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Optimize\n",
    "# This method runs the optimization engine to solve the MIP problem in the model object m\n",
    "m.optimize()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "# The Status attribute  provides current optimization status of the model object m\n",
    "# In workforce model, we check if the model is infeasible or unbounded and report this situation\n",
    "status = m.Status\n",
    "if status == GRB.Status.INF_OR_UNBD or status == GRB.Status.INFEASIBLE  or status == GRB.Status.UNBOUNDED:\n",
    "    print('The model cannot be solved because it is infeasible or unbounded')\n",
    "    sys.exit(0)\n",
    "# If the optimization status of the model is not optimal for some other reason, we report that \n",
    "# situation.\n",
    "if status != GRB.Status.OPTIMAL:\n",
    "    print('Optimization was stopped with status ' + str(status))\n",
    "    sys.exit(0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                    KPI Value\n",
      "0  Total slack required   7.0\n",
      "--------------------------------------------------\n",
      "Shifts\n",
      "  Worker  Number of shifts\n",
      "0    Amy               7.0\n",
      "1    Bob               7.0\n",
      "2  Cathy               7.0\n",
      "3    Dan               8.0\n",
      "4     Ed               7.0\n",
      "5   Fred               7.0\n",
      "6     Gu               8.0\n"
     ]
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "--------------------------------------------------\n",
      "Assigments\n",
      "Symbols: '-': not working, '*': working\n",
      "  worker Mon1 Tue2 Wed3 Thu4 Fri5 Sat6 Sun7 Mon8 Tue9 Wed10 Thu11 Fri12 Sat13 Sun14\n",
      "0    Amy    -    -    *    -    *    -    *    -    -     -     *     *     *     *\n",
      "1    Bob    *    *    -    -    *    *    -    *    -     -     *     -     *     -\n",
      "2  Cathy    -    -    -    *    *    -    *    -    *     *     -     *     *     -\n",
      "3    Dan    -    *    *    -    -    *    -    -    -     *     *     *     *     *\n",
      "4     Ed    *    -    -    *    *    -    *    -    *     -     *     -     *     -\n",
      "5   Fred    *    -    *    -    -    *    -    *    -     -     -     *     *     *\n",
      "6     Gu    -    -    *    -    *    *    *    -    -     *     -     *     *     *\n"
     ]
    }
   ],
   "source": [
    "# Print total slack and the number of shifts worked for each worker\n",
    "# The KPIs for this optimization number is the number of extra worked required to satisfy \n",
    "# demand and the number of shifts that each employed worker is working.\n",
    "solution = {}\n",
    "shifts_sol = {}\n",
    "solution['Total slack required'] = str(totSlack.X)\n",
    "assignments_all = {}\n",
    "gant={}\n",
    "\n",
    "assignments = dict()\n",
    "for [w, s] in availability:\n",
    "    if x[w, s].x == 1:\n",
    "        if w in assignments:\n",
    "            assignments[w].append(s)\n",
    "        else:\n",
    "            assignments[w] = [s]\n",
    "\n",
    "\n",
    "print(pd.DataFrame.from_records(list(solution.items()), columns=['KPI', 'Value']))\n",
    "print('-'*50)\n",
    "\n",
    "for w in workers:\n",
    "    shifts_sol[w]=totShifts[w].X\n",
    "    assignments_all[w]=assignments.get(w, [])\n",
    "\n",
    "print('Shifts')\n",
    "print(pd.DataFrame.from_records(list(shifts_sol.items()), columns=['Worker', 'Number of shifts']))\n",
    "    \n",
    "y_pos = np.arange(len(shifts_sol.keys())) \n",
    "plt.bar(y_pos,shifts_sol.values() , align='center')\n",
    "plt.xticks(y_pos, shifts_sol.keys())\n",
    "plt.show()\n",
    "\n",
    "print('-'*50)\n",
    "for w in assignments_all:\n",
    "    gant[w] = [w]\n",
    "    for d in shifts:\n",
    "        gant[w].append('*' if d in assignments_all[w] else '-')\n",
    "\n",
    "print('Assigments')\n",
    "print('Symbols: \\'-\\': not working, \\'*\\': working')\n",
    "pd.set_option('display.width', 1000)\n",
    "print(pd.DataFrame.from_records(list(gant.values()), columns=['worker']+shifts))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Analysis\n",
    "\n",
    "The total number of extra workers required to satisfy the shift requirements during the planning horizon is seven.\n",
    "\n",
    "The bar chart shows the number of shifts that each employed worker will work during the planning horizon. Observe that with seven extra workers, the workload of employed workers is quite balanced.\n",
    "\n",
    "The last table shows which day during the planning horizon each employed worker is working."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Conclusion\n",
    "\n",
    "In this Jupyter Notebook, we discussed a workforce scheduling optimization problem that is common in the services industry.\n",
    "Consider a service business that develops its workforce plans for the next two weeks. There is only one shift per workday. Each shift may have different workers requirements on each workday. The service business may hire extra workers from an agency to satisfy shift requirements. The service business wants to minimize the number of extra workers that needs to hire, and as a secondary objective, it wants to balance the workload of employed workers.\n",
    "\n",
    "The problem was formulated as a multi-objective mixed-integer-programming (MIP) model,\n",
    "and implemented using the Gurobi Python API and solved using the Gurobi Optimizer.\n",
    "\n",
    "The hierarchical  approach to tackling multi-objective function problems was explained in detail.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Copyright © 2020 Gurobi Optimization, LLC"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
